package detector.home;

import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.graphics.Color;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.TextView.OnEditorActionListener;
import detector.audio.ServerComm;
import detector.audio.processing.AudioClip;
import detector.audio.processing.Monitor;
import detector.audio.processing.PeakHandler;
import detector.audio.processing.SoundClass;

/**
 * Encapsulates common functionality between activities that train To inherit
 * from this, in your subclass, you need to call setContentView in your onCreate
 * method and pass it a layout that includes train_reuse.xml. After calling
 * setContent view, you need to call initializeTrainingViews
 * 
 * @author Kyle
 * 
 */
public abstract class AbstractTrainActivity extends Activity implements
		PeakHandler {
	private static final int NUM_EXAMPLES_REQUIRED = 5;
	protected enum State {
		IDLE, MONITORING, RECORDING
	}

	// enum for the state of a train item
	private enum TrainItemState {
		DISABLED, // record button greyed out, rectangle grey
		RECORDED, // Record button is a green check mark, rectangle green
		READY // record button enabled and has record icon, rectangle green
	}

	protected static final String TAG = "AbstractTrainActivity";
	private static final String DATA_FILE_LOCATION = "audiodata.dat";
	protected State state;
	protected Set<AudioClip> tmpRecord;
	private boolean trainListInitialized = false;

	// Holds the NUM_EXAMPLES_REQUIRED training items - each item is a rectangle
	// and a button for recording
	private View trainViews[];
	// Holds whether each trainView training item is ready to record. True =
	// ready to record,
	// false = already recorded or disabled
	private boolean isReady[];
	// Holds the index of the view of the train item that is currently being
	// recorded into
	private int currentRecordIndex;
	// True if all the training views are greyed out and unclickable
	private boolean isActive;
	private SoundClass toAddExamplesFor;

	// Call this to initialize all the views related to training.
	// It will create the NUM_EXAMPLES_REQUIRED items for the user to record
	// into,
	// set up the event handlers for the relevant buttons and EditTexts, and do
	// other things.
	// Must call this after setContentView
	protected void initializeTrainingViews() {
		trainViews = new View[NUM_EXAMPLES_REQUIRED];
		isReady = new boolean[NUM_EXAMPLES_REQUIRED];

		// Initially not recording
		state = State.IDLE;

		// This will handle peak events as they occur
		Monitor.getMonitor().stopListening();
		Monitor.getMonitor().setPeakHandler(this);

		// initialize tmpRecord
		tmpRecord = new HashSet<AudioClip>();

		// Construct the NUM_EXAMPLES_REQUIRED example recording rectangles and
		// buttons
		// Get the android layout inflater
		LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		// Get the layout that we will be inserting into
		RelativeLayout layout = (RelativeLayout) findViewById(R.id.lytTrainList);

		for (int i = 0; i < NUM_EXAMPLES_REQUIRED; i++) {
			// Get the view from the defined layout to create the view that
			// will be inserted
			final View itemView = inflater.inflate(R.layout.train_list_item,
					null);
			// Set the id, +1 because we can't use zero as an id because it is
			// not a positive number
			itemView.setId(i + 1);

			// Set up the click handler for the button
			// This will store the index of this view in the trainViews array so
			// we
			// can reference it inside the anonymous inner class
			final int index = i;
			itemView.findViewById(R.id.btnRecord).setOnClickListener(
					new OnClickListener() {
						public void onClick(View v) {
							// If the button was in a record state
							// start recording and wait until a sound is
							// recorded.
							// Once a sound is recorded, change the button into
							// a
							// check mark
							ImageButton btnRecord = (ImageButton) v;
							if (isReady[index]) {
								// Start recording if not already
								if (state == State.IDLE) {
									state = State.MONITORING;
									Monitor.getMonitor().startListening();
									currentRecordIndex = index;
								}
							}
						}
					});

			// Set the layout parameters for this train item
			RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
					LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
			//Set it to be centered and give space between each train item
			params.addRule(RelativeLayout.CENTER_HORIZONTAL);
			params.topMargin = (int) TypedValue.applyDimension(
					TypedValue.COMPLEX_UNIT_DIP, 8, AbstractTrainActivity.this
							.getResources().getDisplayMetrics());
			// Only make this train item be below a previous train item if it is
			// not the first
			if (i > 0)
				params.addRule(RelativeLayout.BELOW, i);

			// Add the itemView to the list and the view
			layout.addView(itemView, params);
			trainViews[i] = itemView;

			// Make the item initially look disabled
			setTrainItemState(itemView, TrainItemState.DISABLED);
		}
		trainListInitialized = true;

		// Set up the behavior of the Sound Naming section

		// Get the relevant views
		final Button btnFinish = (Button) findViewById(R.id.btnFinish);
		final EditText txtSoundName = (EditText) findViewById(R.id.txtSoundName);
		RelativeLayout lytNameSound = (RelativeLayout) findViewById(R.id.lytNameSound);

		// Make the sound naming button and text field inivisible initially.
		// They will be enabled when enough examples are recorded
		lytNameSound.setVisibility(View.GONE);

		// Make the finish button disabled
		btnFinish.setEnabled(false);

		// When the user presses enter,
		// hide the keyboard, hide the flashing cursor
		// in txtSoundName
		txtSoundName.setOnEditorActionListener(new OnEditorActionListener() {
			public boolean onEditorAction(TextView tv, int action, KeyEvent arg2) {
				InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
				imm.hideSoftInputFromWindow(txtSoundName.getWindowToken(), 0);
				// stop the flashing cursor from showing
				// up in txtLocation any more
				tv.setCursorVisible(false);
				return true;
			}
		});

		// Make it so the finish button is enabled
		// as the user enters text
		txtSoundName.addTextChangedListener(new TextWatcher() {
			public void afterTextChanged(Editable arg0) {

			}

			public void beforeTextChanged(CharSequence arg0, int arg1,
					int arg2, int arg3) {

			}

			public void onTextChanged(CharSequence arg0, int arg1, int arg2,
					int arg3) {
				btnFinish.setEnabled(true);
			}
		});

		// When the Finish button is clicked, send
		// the recorded examples to the server, popping up a waiting
		// window until that is finished. When finished, finish this activity
		btnFinish.setOnClickListener(new OnClickListener() {
			public void onClick(View v) {
				SoundClass sc = null;
				// Create a new soundclass if this activity wasn't initially
				// called to add examples to an existing soundclass
				if (toAddExamplesFor == null) {
					sc = SoundClass.getSoundClass(txtSoundName.getText()
							.toString());
				} else {
					sc = toAddExamplesFor;
				}
				addToServer(sc);
			}
		});
	}

	/**
	 * If state
	 * 
	 * @param trainItem
	 * @param state
	 */
	private void setTrainItemState(View trainItem, TrainItemState state) {
		// Find the index of the passed trainItem in trainViews
		int index = -1;
		for (int i = 0; i < NUM_EXAMPLES_REQUIRED; i++) {
			if (trainItem.equals(trainViews[i])) {
				index = i;
				break;
			}
		}

		if (state == TrainItemState.READY) {
			// record button enabled and has record icon, monitor on
			isReady[index] = true;
			trainItem.findViewById(R.id.monitorBar).setEnabled(true);
			trainItem.findViewById(R.id.btnRecord).setEnabled(true);
			((ImageButton) trainItem.findViewById(R.id.btnRecord))
					.setBackgroundDrawable(getResources().getDrawable(
							R.drawable.record_button));
		} else if (state == TrainItemState.RECORDED) {
			// Record button is a green check mark, monitor off
			isReady[index] = false;
			trainItem.findViewById(R.id.monitorBar).setEnabled(false);
			trainItem.findViewById(R.id.btnRecord).setEnabled(true);
			((ImageButton) trainItem.findViewById(R.id.btnRecord))
					.setBackgroundDrawable(getResources().getDrawable(
							R.drawable.check_button));
		} else if (state == TrainItemState.DISABLED) {
			// record button greyed out, monitor off
			isReady[index] = false;
			trainItem.findViewById(R.id.monitorBar).setEnabled(false);
			trainItem.findViewById(R.id.btnRecord).setEnabled(false);
			((ImageButton) trainItem.findViewById(R.id.btnRecord))
					.setBackgroundDrawable(getResources().getDrawable(
							R.drawable.record_button_inactive));
		}

	}

	/**
	 * Set all the views in the training section to active or inactive. inactive
	 * means they are greyed out and unclickable active means they are clickable
	 * and not greyed out
	 */
	protected void setTrainSectionActive(boolean isActive) {
		assert (trainListInitialized);
		View rctMask = (View) findViewById(R.id.rctMask);
		if (isActive) {
			rctMask.setVisibility(View.GONE);
			setTrainItemState(trainViews[0], TrainItemState.READY);
		} else {
			rctMask.setVisibility(View.VISIBLE);
		}
		this.isActive = isActive;

	}

	protected boolean isTrainSectionActive() {
		return isActive;
	}

	public void onPeakStarted() {
		assert (trainListInitialized);
		// When a peak is started being recorded, don't do anything for now
		// We might want to alert the user, so it is responsive

		// Just set the state for now
		state = State.RECORDING;
	}

	public void peakDetected(final AudioClip audio) {
		assert (trainListInitialized);
		if (state == State.RECORDING) {
			tmpRecord.add(audio);
			this.runOnUiThread(new Runnable() {
				public void run() {
					// Set the train item that was just recorded into to
					// show it is done, only if the currentRecordIndex is in the
					// array bounds
					if (currentRecordIndex < trainViews.length) {
						setTrainItemState(trainViews[currentRecordIndex],
								TrainItemState.RECORDED);
					}
					// If we haven't recorded all examples yet, enable the
					// next
					// train item,
					// otherwise, display the sound naming section
					currentRecordIndex++;
					if (currentRecordIndex < NUM_EXAMPLES_REQUIRED) {
						setTrainItemState(trainViews[currentRecordIndex],
								TrainItemState.READY);
					} else {
						// Display sound naming view only if
						findViewById(R.id.lytNameSound).setVisibility(
								View.VISIBLE);
						// Enable the finish button
						findViewById(R.id.btnFinish).setEnabled(true);
						// zip up the example section
						// so only one displays
						setTrainItemsZipped(true);
					}

					// Set the state to idle because we are not currently
					// recording audio now
					state = State.IDLE;

					// Stop recording
					Monitor.getMonitor().stopListening();
				}
			});
		}
	}

	/**
	 * if areZipped == true, displays only the first train item. otherwise,
	 * shows all NUM_EXAMPLES_REQUIRED train items
	 * 
	 * @param areZipped
	 */
	private void setTrainItemsZipped(boolean areZipped) {
		if (areZipped) {
			trainViews[0].setVisibility(View.VISIBLE);
			for (int i = 1; i < NUM_EXAMPLES_REQUIRED; i++)
				trainViews[i].setVisibility(View.GONE);
		} else {
			for (int i = 0; i < NUM_EXAMPLES_REQUIRED; i++)
				trainViews[i].setVisibility(View.VISIBLE);
		}
	}

	/**
	 * If this is called in the child's onCreate, the sound naming step will be
	 * skipped if sc != null. Instead, it will just display the sound's name and
	 * the finish button. This handles making the train section active and
	 * visible and all the UI stuff to implement the behavior of adding examples
	 * to a sound.
	 * 
	 * @param sc
	 *            SoundClass to add examples for
	 */
	protected void setSoundClassToAddExamplesFor(SoundClass sc) {
		this.toAddExamplesFor = sc;
		if (toAddExamplesFor != null) {
			// Make the naming EditText invisible
			((EditText) findViewById(R.id.txtSoundName))
					.setVisibility(View.GONE);
			((TextView) findViewById(R.id.lblSoundPrompt))
					.setVisibility(View.GONE);
			// Make the sound name label visible and set the name
			TextView lblSoundName = (TextView) findViewById(R.id.lblSoundName);
			lblSoundName.setVisibility(View.VISIBLE);
			lblSoundName.setText(sc.getName());

			// Make the sound name view visible, we don't need to wait for the
			// user to make all the examples
			findViewById(R.id.lytNameSound).setVisibility(View.VISIBLE);

			// Make sure the example list is expanded and active
			setTrainItemsZipped(false);
			setTrainSectionActive(true);

		}
	}

	/**
	 * Pop up a toast message with the specified message and duration
	 * 
	 * @param msg
	 *            msg to toast
	 * @param duration
	 *            duration to toast for
	 */
	private void toastMsg(String msg, int duration) {
		Context context = getApplicationContext();
		CharSequence text = msg;
		Toast toast = Toast.makeText(context, text, duration);
		toast.show();
	}

	// Save the audio clip buffer to file to view what was recorded
	private void saveAudioData(AudioClip audio) {
		assert (trainListInitialized);
		FileOutputStream fos;
		PrintWriter out;
		try {
			fos = this.openFileOutput(DATA_FILE_LOCATION,
					Context.MODE_WORLD_READABLE);
			out = new PrintWriter(fos);
			for (int i = 0; i < audio.getBytes().length; i += 2)
				out.println((short) audio.getBytes()[i]
						+ (short) audio.getBytes()[i + 1] << 8);

			out.close();
			fos.close();
		} catch (Exception e) {
			this.runOnUiThread(new Runnable() {
				public void run() {
					// Report that an error happened using a toast
					Context context = getApplicationContext();
					CharSequence text = "Failed to save application data.";
					int duration = Toast.LENGTH_SHORT;
					Toast toast = Toast.makeText(context, text, duration);
					toast.show();
				}
			});
		}
	}

	/**
	 * Adds the examples in tmpRecord to the server under the passed soundclass,
	 * showing a toast describing the result (exception or success) and popping
	 * up a waiting window while that is happening. Once done, it finishes the
	 * activity. This method will add the exampleIds the server returns to
	 * toTrain if successful.
	 * 
	 * Make sure this is called on the UI thread, and make sure the activity is
	 * not finished
	 * 
	 * @param toTrain
	 */
	protected void addToServer(final SoundClass toTrain) {
		assert (trainListInitialized);
		// Popup a waiting window
		final ProgressDialog waitDialog = ProgressDialog.show(
				AbstractTrainActivity.this, "Sending",
				"Sending Examples to Server", true, false);

		// Now start a thread to send training examples
		// We need to do this, otherwise the UI thread won't start the
		// waitDialog because it is spending cycles doing the
		// sendTrainingExamples
		Thread t = new Thread(new Runnable() {
			public void run() {
				try {
					ServerComm.sendTrainingExamples(toTrain, tmpRecord);
					AbstractTrainActivity.this.setResult(RESULT_OK);
					AbstractTrainActivity.this.finish();
				} catch (final Exception ex) {
					Log.e(TAG, Log.getStackTraceString(ex));
					AbstractTrainActivity.this.runOnUiThread(new Runnable() {
						private String TAG = "ServerComm.addToServer";

						public void run() {
							// Toast that exception happened
							Context context = AbstractTrainActivity.this
									.getApplicationContext();
							CharSequence text = "Failed to contact server";
							ex.printStackTrace();
							int duration = Toast.LENGTH_SHORT;

							Toast toast = Toast.makeText(context, text,
									duration);
							toast.show();
							Log.e(TAG, "failed to train: " + ex.getMessage());
						}
					});
				} finally {
					AbstractTrainActivity.this.runOnUiThread(new Runnable() {
						public void run() {
							try {
								waitDialog.dismiss();
							} catch (Exception ex) {
								// hack to handle dismissing a waitDialog after
								// window is closed
							}
						}
					});
				}
			}
		});
		t.start();
	}
}
